Curator Affiliate Link Creation API
概述 | Overview
该接口用于 Curator(策展人/店主)为自己拥有的 Post 创建推广联盟链接,通过指定 Promoter(推广者)的手机号或邮箱,建立双方的合作关联。
接口信息 | Endpoint Information
| 属性 | 值 |
|---|---|
| 请求地址 | /promoter-association/curator/affiliate-link |
| 请求方式 | POST |
| 认证方式 | JWT Bearer Token (Required) |
| Content-Type | application/json |
请求头 | Request Headers
| Header | 类型 | 必填 | 说明 |
|---|---|---|---|
Authorization |
string |
✅ | JWT Bearer Token (Bearer <token>) |
Content-Type |
string |
✅ | 固定值 application/json |
from |
string |
- | 客户端标识 (示例值: client) |
timezone |
string |
- | 时区信息 (示例值: Asia/Shanghai) |
x-track-id |
string |
- | 追踪 ID (用于日志关联) |
示例
POST /promoter-association/curator/affiliate-link HTTP/1.1
Host: release.katana-api.1m.app
Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...
Content-Type: application/json
from: client
timezone: Asia/Shanghai
x-track-id: 1540447c-78a1-4d7a-a2c1-a523d659cc20
请求参数 | Request Parameters
Body (JSON)
| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
curatorId |
string (UUID) |
✅ | Curator 用户 ID(必须与当前认证用户 ID 一致) |
postAlias |
string |
✅ | Post 的 URL 别名(Post 必须属于该 Curator 且允许转售) |
phoneNumberOrEmail |
string |
✅ | Promoter 的手机号或邮箱(需已验证) |
note |
string |
- | 备注(可选,用于记录合作信息) |
请求示例
{
"phoneNumberOrEmail": "neo.wang.ext+20@1m.app",
"curatorId": "1ee2b015-5390-44ba-a677-8cbd53e8066f",
"postAlias": "000011",
"note": "合作备注信息"
}
响应结构 | Response Structure
成功响应 (200 OK)
interface CreateAffiliateLinkResponse {
// Curator 信息
curator: {
id: string; // Curator 用户 ID
vanityUrl: string; // Curator 店铺别名 URL
};
// Post 信息
post: {
id: string; // Post 数据库 ID
alias: string; // Post URL 别名
};
// Promoter 信息
promoter: {
id: string; // Promoter 用户 ID
email: string; // Promoter 邮箱
phoneNumber: string; // Promoter 手机号
vanityUrl: string; // Promoter 店铺别名 URL
};
// 推广相关
affiliateCode: string; // Promoter 推广码
note?: string; // 备注(如有)
}
响应示例
{
"curator": {
"id": "1ee2b015-5390-44ba-a677-8cbd53e8066f",
"vanityUrl": "curator-store"
},
"post": {
"id": "post-uuid-12345",
"alias": "000011"
},
"promoter": {
"id": "promoter-uuid-67890",
"email": "neo.wang.ext+20@1m.app",
"phoneNumber": "+1234567890",
"vanityUrl": "promoter-store"
},
"affiliateCode": "AFF123456",
"note": "合作备注信息"
}
错误响应 | Error Responses
400 Bad Request - 参数验证失败
{
"statusCode": 400,
"message": ["phoneNumberOrEmail must be a valid email or phone number"],
"error": "Bad Request"
}
400 Bad Request - 权限不足
{
"statusCode": 400,
"message": "You do not have the permission to perform this operation.",
"error": "Bad Request"
}
[!warning] 权限说明
curatorId必须与当前 JWT Token 中的用户 ID 一致,否则返回此错误。
400 Bad Request - 不能为 Post 所有者创建链接
{
"statusCode": 400,
"message": "Can not create affiliate-link for the post owner.",
"error": "Bad Request"
}
400 Bad Request - Promoter 已转售该 Post
{
"statusCode": 400,
"message": "The promoter has already resold this post",
"error": "Bad Request"
}
400 Bad Request - 不能为商品所有者创建链接
{
"statusCode": 400,
"message": "Can not create affiliate-link for the product owner",
"error": "Bad Request"
}
404 Not Found - Curator 或 Post 不存在
{
"statusCode": 404,
"message": "Resource not found",
"error": "Not Found"
}
业务逻辑 | Business Logic
核心流程图
flowchart TD
A[接收请求] --> B[验证 JWT Token]
B --> C{curatorId == userId?}
C -->|否| D[返回 400 权限错误]
C -->|是| E[验证 phoneNumberOrEmail 格式]
E -->|无效| F[返回 400 参数错误]
E -->|有效| G[并行查询 Curator、Post、Promoter]
G --> H{Curator/Post 存在?}
H -->|否| I[返回 404]
H -->|是| J{Promoter == Curator?}
J -->|是| K[返回 400 不能为所有者创建]
J -->|否| L{Promoter 已转售 Post?}
L -->|是| M[返回 400 已转售]
L -->|否| N{Promoter 是商品所有者?}
N -->|是| O[返回 400 不能为商品所有者创建]
N -->|否| P{Promoter 存在?}
P -->|否| Q[创建新用户<br/>role=CONSUMER<br/>type=GUEST]
P -->|是| R[使用现有 Promoter]
Q --> S[创建/更新<br/>PromoterAssociation]
R --> S
S --> T[发送事件<br/>AddPostAccessPromoter]
T --> U[返回响应]
详细处理步骤
0. 权限验证
const userId = this.requestContextService.getUserId();
if (userId !== curatorId) {
throw new BadRequestException('You do not have the permission to perform this operation.');
}
1. 参数格式验证
使用 class-validator 验证 phoneNumberOrEmail 必须是有效的邮箱或手机号格式。
2. 并行数据查询
const [curator, post, promoter] = await Promise.all([
this.userMetaRepository.findUserEntityById(curatorId),
this.promoterAssociationRepository.findPostByAliasAndCreatorId({
urlAlias: postAlias,
creatorId: curatorId,
allowPromotersResell: true, // Post 必须允许转售
}),
this.userMetaRepository.findAffiliateOrVerifiedUserByPhoneNumberOrEmail(phoneNumberOrEmail),
]);
3. 业务规则验证
| 规则 | 说明 | 错误码 |
|---|---|---|
| Curator 必须存在 | 查询用户表 | 404 |
| Post 必须存在且允许转售 | allowPromotersResell = true |
404 |
| 不能为 Post 所有者创建 | curator.userId !== promoter.userId |
400 |
| 防止重复转售 | 检查 post.uplineCreatorList |
400 |
| 不能为商品所有者创建 | 检查 post.relatedProducts |
400 |
4. Promoter 用户创建
如果 Promoter 不存在,自动创建新用户:
| 属性 | 值 |
|---|---|
userRole |
CONSUMER |
type |
GUEST |
createdFeature |
AFFILIATE |
email / phoneNumber |
来自请求参数 |
5. PromoterAssociation 创建/更新
await this.promoterAssociationService.createPromoterAssociation({
promoterId: promoterUserEntity.userId,
curatorId,
note,
});
服务层会:
- 查找 Promoter 最新的 POST 或 MIXED 类型的 StoreFrontModule
- 创建新的 PromoterAssociation 或更新已删除的关联
- 发送
AddPostAccessPromoter事件(用于受限 Post 的访问权限)
关键数据模型 | Data Models
PromoterAssociation
model PromoterAssociation {
id String @id
promoterId String
curatorId String
invitationType InvitationType @default(AFFILIATE)
invitationCode String?
allowSyncPosts Boolean @default(true)
syncStoreFrontModuleIds String[]
note String?
deletedAt DateTime?
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
// Relations
promoter User @relation("PromoterAssociations_PromoterId")
curator User @relation("PromoterAssociations_CuratorId")
@@unique([promoterId, curatorId])
}
InvitationType
enum InvitationType {
INVITATION_LINK // 标准推广链接
AFFILIATE // 联盟推广链接
}
注意事项 | Important Notes
[!danger] 不可逆操作
- 创建 PromoterAssociation 后会发送
AddPostAccessPromoter事件- 受限 Post (
isAccessRestricted = true) 会自动添加 Promoter 到访问列表- 需通过断开连接接口才能撤销关联
[!warning] 用户类型升级
- 新创建的用户初始为
role=CONSUMER, type=GUEST- 当有订单产生时,会自动升级为
role=PROMOTER, type=AFFILIATE(见updateRoleAndTypeFromOrder)[!tip] StoreFront 同步
allowSyncPosts默认为truesyncStoreFrontModuleIds自动设置为 Promoter 最新的 POST/MIXED 模块[!info] 相关接口
POST /promoter-association/guest/affiliate-link- 公开接口,无需认证GET /promoter-association/curators- 获取关联的 Curator 列表DELETE /promoter-association/curator/:curatorId- 断开连接
测试用例 | Test Cases
成功场景
| 场景 | 预期结果 |
|---|---|
| 为新用户(不存在)创建链接 | 创建新用户 + PromoterAssociation,返回 200 |
| 为已验证用户创建链接 | 创建 PromoterAssociation,返回 200 |
| 添加 note 参数 | note 保存到 PromoterAssociation |
失败场景
| 场景 | 预期结果 |
|---|---|
| curatorId 与当前用户不一致 | 400 权限错误 |
| Post 不存在 | 404 |
| Post 不允许转售 | 404 |
| 为 Post 所有者创建链接 | 400 |
| Promoter 已在 Post 的转售链中 | 400 |
| Promoter 是 Post 的商品所有者 | 400 |
| phoneNumberOrEmail 格式无效 | 400 |
相关文件 | Related Files
| 文件路径 | 说明 |
|---|---|
src/promoter-association/promoter-association.controller.ts |
控制器 (第 298-384 行) |
src/promoter-association/promoter-association.service.ts |
服务层 (第 51-83 行) |
src/promoter-association/promoter-association.repo.ts |
数据访问层 (第 28-99 行) |
src/promoter-association/promoter-association.interface.ts |
DTO 定义 (第 16-58 行) |
变更历史 | Changelog
| 版本 | 日期 | 变更内容 |
|---|---|---|
| 1.0.0 | 2026-02-26 | 初始版本,支持创建联盟推广链接 |
相关 JIRA | Related Tickets
- [[KAT-10550]] - View all affiliate post links
- [[KAT-6637]] - Affiliate users should not show in Collaboration